package com.code.coinprice.handlers;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.code.coinprice.configs.NettyConfig;
import com.code.coinprice.consts.CommonConstant;
import com.code.coinprice.managers.CoinPriceManager;
import com.code.coinprice.models.RelativeCoinPrice;
import com.code.coinprice.services.CoinPriceSourceService;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.util.AttributeKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 处理webSocket请求的channelHandler
 * <p>
 * Netty框架的目标就是让你的业务逻辑从网络基础应用编码中分离出来，让你可以专注业务的开
 * 发，而不需写一大堆类似NIO的网络处理操作，大堆类似NIO的网络处理操作在NettyServer中统一定义即可
 * ChannelHandler 是一个接口，处理 I/O 事件或拦截 I/O 操作，并将其转发到其 ChannelPipeline(业务处理链)中的
 * 下一个处理程序。
 * ChannelHandler 本身并没有提供很多方法，因为这个接口有许多的方法需要实现，方便使用期间，可以继承它的子
 * 类：
 * 1 ChannelInboundHandler 用于处理入站 I/O 事件。
 * 2 ChannelOutboundHandler 用于处理出站 I/O 操作。
 * 或者使用以下适配器类：
 * 1 ChannelInboundHandlerAdapter 用于处理入站 I/O 事件。
 * 2 ChannelOutboundHandlerAdapter 用于处理出站 I/O 操作。
 */
@Component
@ChannelHandler.Sharable
public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

    private static final Logger log = LoggerFactory.getLogger(WebSocketHandler.class);

    @Resource
    private CoinPriceManager coinPriceManager;

    @Resource
    private RedisHandler redisHandler;

    @Resource
    private CoinPriceSourceService coinPriceSourceService;

    /**
     * 一旦连接，第一个被执行
     * 新建立的连接会按照初始化策略，把handler添加到该channel的pipeline里面，也就是ch.pipeline().addLast(webSocketHandler)执行完成后的回调；
     *
     * @param ctx 上下文对象, 含有通道channel，管道pipeline
     */
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) {
        Channel channel = ctx.channel();
        log.info("handlerAdded 被调用:{}", channel.id().asLongText());
        // 添加到channelGroup 通道组
        NettyConfig.getChannelGroup().add(channel);
    }

    /**
     * 读取客户端发送的数据
     * 客户端向服务端发来数据，每次都会回调此方法，表示有数据可读
     *
     * @param ctx 保存 Channel 相关的所有上下文信息，同时关联一个 ChannelHandler 对象，上下文对象, 含有通道channel，管道pipeline
     * @param msg 就是客户端发送的数据
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) {
        String msgText = msg.text();
        log.info("服务器收到消息：{}", msgText);

        // 获取平台标识,关联channel
        JSONObject msgJsonObject = JSONObject.parseObject(msgText);
        if (null != msgJsonObject) {
            Channel channel = ctx.channel();
            String clientChannelId = msgJsonObject.getString(CommonConstant.CLIENT_CHANNEL_ID);
            if (StringUtils.isEmpty(clientChannelId)) {
                channel.writeAndFlush(new TextWebSocketFrame("clientChannelId can not empty!"));
                return;
            }
            String platformTag = msgJsonObject.getString(CommonConstant.PLATFORM_TAG);
            if (coinPriceSourceService.getCoinPriceSourceBySource(platformTag) == null) {
                channel.writeAndFlush(new TextWebSocketFrame("not exist source,can not create a connect for current client!"));
                return;
            }
            redisHandler.zsetAdd(platformTag, clientChannelId, 1);
            redisHandler.set(clientChannelId, platformTag);
            NettyConfig.getPlatformChannelMap().put(clientChannelId, channel);
            // 将客户端通道唯一标识作为自定义属性加入到channel中，方便随时channel中获取客户端的通道唯一标识
            AttributeKey<String> key = AttributeKey.valueOf(CommonConstant.CLIENT_CHANNEL_ID);
            channel.attr(key).setIfAbsent(clientChannelId);
            String coinType = msgJsonObject.getString(CommonConstant.COIN_TYPE);
            List<RelativeCoinPrice> relativeCoinPrices = coinPriceManager.getCoinPriceListBySource(platformTag);
            if (!StringUtils.isEmpty(coinType)) {
                relativeCoinPrices = relativeCoinPrices.stream().filter(item -> coinType.equalsIgnoreCase(item.getCoinName())).collect(Collectors.toList());
                redisHandler.set(clientChannelId + ":" + CommonConstant.COIN_TYPE, coinType);
            }
            channel.writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(relativeCoinPrices)));
        }

    }

    /**
     * 数据读取完毕处理方法
     * 服务端每次读完一次完整的数据之后，回调该方法，表示数据读取完毕；
     *
     * @param ctx 上下文对象, 含有通道channel，管道pipeline
     */
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.flush();
    }

    /**
     * 对应handlerAdded，将handler从该channel的pipeline移除后的回调方法。
     *
     * @param ctx 上下文对象, 含有通道channel，管道pipeline
     */
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) {
        Channel channel = ctx.channel();
        log.info("handlerRemoved 被调用:{}", channel.id().asLongText());
        // 删除通道
        NettyConfig.getChannelGroup().remove(channel);
        removeClientChannelId(ctx);
    }

    /**
     * 处理异常, 一般是需要关闭通道
     *
     * @param ctx   上下文对象, 含有通道channel，管道pipeline
     * @param cause 异常
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        log.info("异常：{}", cause.getMessage());
        // 删除通道
        NettyConfig.getChannelGroup().remove(ctx.channel());
        removeClientChannelId(ctx);
        ctx.close();
    }

    /**
     * 删除用户与channel的对应关系
     *
     * @param ctx 上下文对象, 含有通道channel，管道pipeline
     */
    private void removeClientChannelId(ChannelHandlerContext ctx) {
        AttributeKey<String> key = AttributeKey.valueOf(CommonConstant.CLIENT_CHANNEL_ID);
        String clientChannelId = ctx.channel().attr(key).get();
        if (!StringUtils.isEmpty(clientChannelId)) {
            NettyConfig.getPlatformChannelMap().remove(clientChannelId);
            Object platformTag = redisHandler.get(clientChannelId);
            if (platformTag != null) {
                redisHandler.zsetRemove(String.valueOf(platformTag), clientChannelId);
                redisHandler.del(clientChannelId);
                redisHandler.del(clientChannelId + ":" + CommonConstant.COIN_TYPE);
            }
        }
    }
}
